package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.RedisData;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.SystemConstants;
import com.sun.org.apache.regexp.internal.RE;
import io.lettuce.core.ScriptOutputType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Stream;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private CacheClient cacheClient;
    @Resource
    private ShopMapper shopMapper;

    /**
     * 店铺的缓存
     * @param id
     * @return
     */
    @Override
    public Result queryByid(Long id) {
//        缓存穿透
//        Shop shop = cacheClient
//                .queryWithPassThrough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, id2 -> getById(id2), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

//        缓存击穿 逻辑过期
//        Shop shop = cacheClient.queryWithLogicalEpriex(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//     缓存击穿 加锁
        Shop shop = queryWithMutex(id);
        if(shop==null) {
            return Result.fail("没有该店铺的信息");
        }
        return  Result.ok(shop);
    }


    /**
     * 加锁解决缓存击穿
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id) {
//        先从缓存中查询
        String s= RedisConstants.CACHE_SHOP_KEY + id;
        String strJson = stringRedisTemplate.opsForValue().get(s);
//        查到了就
        if(StrUtil.isNotBlank(strJson)) {
            return JSONUtil.toBean(strJson, Shop.class);
        }
//        "" != null 是true
//        如过第一次查询id穿透了，strJson = null if语句走不通，第二次  strJson=”“，if语句为true
        if (strJson != null) {
            return null;
        }
        try {
//        缓存没查到，获取锁
            boolean islock = tryLock(RedisConstants.LOCK_SHOP_KEY+id);
//        获取锁失败
            if (!islock) {
            Thread.sleep(50);
            return  queryWithMutex(id);
            }
//        锁获取成功，查询数据库
            Shop shop = getById(id);
//           本地测试时候打开 Thread.sleep(200);
            if(shop==null) {
//        设置空值解决：缓存穿透
                stringRedisTemplate.opsForValue().set(s,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            stringRedisTemplate.opsForValue().set(s,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
            return shop;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
//            释放锁
            unlock(RedisConstants.LOCK_SHOP_KEY+id);
        }
    }
    /**
     * 解决缓存穿透
     * @param id
     * @return
     */
    public Shop querychuangtuo(Long id) {
        //        先从缓存中查询
        String s= RedisConstants.CACHE_SHOP_KEY + id;
        String strJson = stringRedisTemplate.opsForValue().get(s);
//        查到了就
        if(StrUtil.isNotBlank(strJson)) {
            return (JSONUtil.toBean(strJson, Shop.class));
        }
//        "" != null 是true
//        如何第一次查询id穿透了，strJson = null if语句走不通，第二次  strJson=”“，if语句为true
        if (strJson != null) {
            return null;
        }
//        没查到，就从数据库里查
        Shop shop = getById(id);
        if(shop==null) {
//            设置空值解决缓存穿透
            stringRedisTemplate.opsForValue().set(s,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }
        stringRedisTemplate.opsForValue().set(s,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return shop;
    }

    /**
     * 线程池
     */
    private  static  final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    /**
     * 逻辑过期
     * @param id
     * @return
     */
    public Shop queryLuojiGuoqi(Long id) {
//        先从缓存中查询
        String s= RedisConstants.CACHE_SHOP_KEY + id;
        String strJson = stringRedisTemplate.opsForValue().get(s);
//        没查到了就
        if(StrUtil.isBlank(strJson)) {
            return null;
        }
//        查到了
        RedisData redisData = JSONUtil.toBean(strJson, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getObject(), Shop.class);
        LocalDateTime expire = redisData.getExpire();
//       判断是否过期
        if(expire.isAfter(LocalDateTime.now())) {
//        没过期
            return shop;
        }
//       过期了 1.获取锁
        boolean lock = tryLock(RedisConstants.LOCK_SHOP_KEY + id);
//        1.1获取锁成功
        if(lock) {
//            开启独立线程
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
//                    重建缓存
                    showShop(id,20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
//                    释放锁
                    unlock(RedisConstants.LOCK_SHOP_KEY + id);
                }
            });
        }
        return shop;
    }
    /**
     * 逻辑过期设置
     * @param id
     * @param expiretime
     */
    public void showShop(Long id,Long expiretime) {
        Shop shop = getById(id);
        RedisData redisData=new RedisData();
        redisData.setObject(shop);
        redisData.setExpire(LocalDateTime.now().plusSeconds(expiretime));
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(redisData));
    }
    /**
     * 获取锁
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
//        setIfAbsent这个方法是key不存在才可以存储。key存在反而不能存储
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
//        解决拆箱
        return BooleanUtil.isTrue(aBoolean);
    }

    /**
     * 释放锁
     * @param key
     */
    private  void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
    /**
     * 更新店铺
     * @param shop
     * @return
     */
    @Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if(id == null) {
            return Result.fail("店铺id不能为空");
        }
//        先更新数据库，在删除缓存
        updateById(shop);
        stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY +id);
        return Result.ok();
    }

    /**
     * 根据坐标查询周围店铺
     * @param typeId
     * @param current
     * @param x
     * @param y
     * @return
     */
    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        //先判断是否需要根据范围查询
        if(x == null || y == null) {
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            return  Result.ok(page.getRecords());
        }
        //计算分页页码
        int from = (current-1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end  = current * SystemConstants.DEFAULT_PAGE_SIZE;
        // limit的end:代表数据从0查到end结束，
        // 并没有按照我们的期望即 查到d的数据从form开始end结束（完成分页）
        // 为了达到分页的目的，我们用stream流的skip实现
        GeoResults<RedisGeoCommands.GeoLocation<String>> radius = stringRedisTemplate.opsForGeo().radius(
                RedisConstants.SHOP_GEO_KEY + typeId,
                new Circle(new Point(x,y),new Distance(5000,Metrics.NEUTRAL)),
                RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().limit(end)
        );
        if(radius == null) {
            return  Result.ok(Collections.emptyList());
        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = radius.getContent();
        if(list.size() <=  from) {
        //    没有下一页了
            return Result.ok(Collections.emptyList());
        }
        //存放解析到的ShopId
        List<Long> listId = new ArrayList<>(list.size());
        //把店铺id 和 对应的距离联系起来
        Map<String,Distance> map= new HashMap<>(list.size());
        list.stream().skip(from).forEach(result ->{
            //获取店铺id
            String Shopid = result.getContent().getName();
            listId.add(Long.valueOf(Shopid));
            Distance distance = result.getDistance();
            map.put(Shopid,distance);
        });
        List<Shop> shopList = shopMapper.queryByIds(listId);
        //店铺Set上距离
        shopList.forEach(shop -> {
            shop.setDistance(map.get(shop.getId().toString()).getValue());
        });
        //返回结果
        return Result.ok(shopList);
    }
}
